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.ibm.icu.text.Collator;
55 public class DictionaryActivity extends ListActivity {
58 // * Download latest dicts.
59 // * http://ftp.tu-chemnitz.de/pub/Local/urz/ding/de-en-devel/
60 // * http://www1.dict.cc/translation_file_request.php?l=e
61 // * Compress all the strings everywhere, put compression table in file.
63 // * Only one way to way for current search to end. (won't do).
65 static final String LOG = "QuickDic";
66 static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
67 static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
70 final Handler uiHandler = new Handler();
71 private final Executor searchExecutor = Executors.newSingleThreadExecutor();
75 int lastSelectedRow = 0; // TODO: I'm evil.
77 private boolean prefsMightHaveChanged = true;
80 private File wordList;
81 private RandomAccessFile dictRaf = null;
82 private Dictionary dictionary = null;
83 private boolean saveOnlyFirstSubentry = false;
85 // Visible for testing.
86 LanguageListAdapter languageList = null;
87 private SearchOperation searchOperation = null;
89 public DictionaryActivity() {
91 searchExecutor.execute(new Runnable() {
93 final long startMillis = System.currentTimeMillis();
94 for (final String lang : Arrays.asList("EN", "DE")) {
95 Language.lookup(lang).getFindCollator();
96 final Collator c = Language.lookup(lang).getSortCollator();
97 if (c.compare("pre-print", "preppy") >= 0) {
98 Log.e(LOG, c.getClass() + " is buggy, lookups may not work properly.");
101 Log.d(LOG, "Loading collators took:" + (System.currentTimeMillis() - startMillis));
107 /** Called when the activity is first created. */
109 public void onCreate(Bundle savedInstanceState) {
110 super.onCreate(savedInstanceState);
111 Log.d(LOG, "onCreate:" + this);
114 initDictionaryAndPrefs();
115 } catch (Exception e) {
121 setContentView(R.layout.main);
122 searchText = (EditText) findViewById(R.id.SearchText);
123 langButton = (Button) findViewById(R.id.LangButton);
125 Log.d(LOG, "adding text changed listener");
126 searchText.addTextChangedListener(new SearchTextWatcher());
128 getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
129 public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
133 public void onNothingSelected(AdapterView<?> arg0) {
137 getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
138 public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
145 final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
146 clearSearchTextButton.setOnClickListener(new OnClickListener() {
147 public void onClick(View v) {
148 onClearSearchTextButton(clearSearchTextButton);
151 clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
152 getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
155 final Button langButton = (Button) findViewById(R.id.LangButton);
156 langButton.setOnClickListener(new OnClickListener() {
157 public void onClick(View v) {
162 final Button upButton = (Button) findViewById(R.id.UpButton);
163 upButton.setOnClickListener(new OnClickListener() {
164 public void onClick(View v) {
168 final Button downButton = (Button) findViewById(R.id.DownButton);
169 downButton.setOnClickListener(new OnClickListener() {
170 public void onClick(View v) {
176 registerForContextMenu(getListView());
181 private void initDictionaryAndPrefs() throws Exception {
182 if (!prefsMightHaveChanged) {
185 closeCurrentDictionary();
187 final SharedPreferences prefs = PreferenceManager
188 .getDefaultSharedPreferences(this);
189 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
190 getString(R.string.wordListFileDefault)));
191 Log.d(LOG, "wordList=" + wordList);
193 saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
195 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
196 getString(R.string.dictFileDefault)));
197 Log.d(LOG, "dictFile=" + dictFile);
200 if (!dictFile.canRead()) {
201 throw new IOException("Unable to read dictionary file.");
204 dictRaf = new RandomAccessFile(dictFile, "r");
205 final long startMillis = System.currentTimeMillis();
206 dictionary = new Dictionary(dictRaf);
207 Log.d(LOG, "Read dictionary millis: " + (System.currentTimeMillis() - startMillis));
208 } catch (IOException e) {
209 Log.e(LOG, "Couldn't open dictionary.", e);
210 this.startActivity(new Intent(this, NoDictionaryActivity.class));
212 throw new Exception(e);
215 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, SimpleEntry.LANG1) == SimpleEntry.LANG1 ? SimpleEntry.LANG1
218 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
219 setListAdapter(languageList);
220 prefsMightHaveChanged = false;
224 public void onResume() {
226 Log.d(LOG, "onResume:" + this);
229 initDictionaryAndPrefs();
230 } catch (Exception e) {
234 final SharedPreferences prefs = PreferenceManager
235 .getDefaultSharedPreferences(this);
236 final String searchTextString = prefs
237 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
238 searchText.setText(searchTextString);
239 getListView().requestFocus();
240 onSearchTextChange(searchTextString);
244 public void onPause() {
246 Log.d(LOG, "onPause:" + this);
247 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
249 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
250 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
255 public void onStop() {
257 Log.d(LOG, "onStop:" + this);
259 Log.i(LOG, "isFinishing()==true, closing dictionary.");
260 closeCurrentDictionary();
264 private void closeCurrentDictionary() {
265 Log.i(LOG, "closeCurrentDictionary");
266 if (dictionary == null) {
271 setListAdapter(null);
272 Log.d(LOG, "setListAdapter finished.");
275 if (dictRaf != null) {
278 } catch (IOException e) {
279 throw new RuntimeException(e);
284 public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
285 final Row row = languageList.languageData.rows.get(getSelectedRow());
286 return languageList.languageData.rowToString(row, onlyFirstSubentry);
289 // ----------------------------------------------------------------
291 // ----------------------------------------------------------------
293 private MenuItem switchLanguageMenuItem = null;
296 public boolean onCreateOptionsMenu(final Menu menu) {
297 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
298 switchLanguageMenuItem
299 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
300 public boolean onMenuItemClick(final MenuItem menuItem) {
306 final MenuItem preferences = menu.add(getString(R.string.preferences));
307 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
308 public boolean onMenuItemClick(final MenuItem menuItem) {
309 prefsMightHaveChanged = true;
310 startActivity(new Intent(DictionaryActivity.this,
311 PreferenceActivity.class));
316 final MenuItem about = menu.add(getString(R.string.about));
317 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
318 public boolean onMenuItemClick(final MenuItem menuItem) {
319 final Intent intent = new Intent().setClassName(AboutActivity.class
320 .getPackage().getName(), AboutActivity.class.getCanonicalName());
321 final String currentDictInfo;
322 if (dictionary == null) {
323 currentDictInfo = getString(R.string.noDictLoaded);
325 final LanguageData lang0 = dictionary.languageDatas[0];
326 final LanguageData lang1 = dictionary.languageDatas[1];
327 currentDictInfo = getString(R.string.aboutText, dictionary.dictionaryInfo, dictionary.entries.size(),
328 lang0.language.symbol, lang0.sortedIndex.size(), lang0.rows.size(),
329 lang1.language.symbol, lang1.sortedIndex.size(), lang1.rows.size());
331 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
333 startActivity(intent);
338 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
339 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
340 public boolean onMenuItemClick(final MenuItem menuItem) {
341 prefsMightHaveChanged = true;
342 startDownloadDictActivity(DictionaryActivity.this);
351 public boolean onPrepareOptionsMenu(final Menu menu) {
352 switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
353 dictionary.languageDatas[SimpleEntry
354 .otherLang(languageList.languageData.lang)].language.symbol));
355 return super.onPrepareOptionsMenu(menu);
358 void updateLangButton() {
359 langButton.setText(languageList.languageData.language.symbol);
362 // ----------------------------------------------------------------
364 // ----------------------------------------------------------------
366 void onLanguageButton() {
368 languageList = new LanguageListAdapter(
369 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
371 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
372 setListAdapter(languageList);
374 onSearchTextChange(searchText.getText().toString());
378 final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
379 Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
380 jumpToRow(languageList, destRowIndex);
383 void onDownButton() {
384 final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
385 Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
386 jumpToRow(languageList, destRowIndex);
389 void onAppendToWordList() {
390 final int row = getSelectedRow();
394 final StringBuilder rawText = new StringBuilder();
395 final String word = languageList.languageData.getIndexEntryForRow(row).word;
397 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
399 rawText.append(word).append("\t");
400 rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
401 Log.d(LOG, "Writing : " + rawText);
403 wordList.getParentFile().mkdirs();
404 final PrintWriter out = new PrintWriter(
405 new FileWriter(wordList, true));
406 out.println(rawText.toString());
408 } catch (IOException e) {
409 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
410 Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
416 final int row = getSelectedRow();
420 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
421 final StringBuilder result = new StringBuilder();
422 result.append(getSelectedRowRawText(false));
423 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
424 clipboardManager.setText(result.toString());
425 Log.d(LOG, "Copied: " + result);
429 public boolean onKeyDown(int keyCode, KeyEvent event) {
430 if (event.getUnicodeChar() != 0) {
431 if (!searchText.hasFocus()) {
432 searchText.setText("" + (char) event.getUnicodeChar());
433 onSearchTextChange(searchText.getText().toString());
434 searchText.requestFocus();
438 return super.onKeyDown(keyCode, event);
442 protected void onListItemClick(ListView l, View v, int row, long id) {
444 openContextMenu(getListView());
447 void onSearchTextChange(final String searchText) {
448 Log.d(LOG, "onSearchTextChange: " + searchText);
449 synchronized (this) {
450 searchOperation = new SearchOperation(languageList, searchText.trim(), searchOperation);
451 searchExecutor.execute(searchOperation);
455 private void onClearSearchTextButton(final Button clearSearchTextButton) {
456 clearSearchTextButton.requestFocus();
457 searchText.setText("");
458 searchText.requestFocus();
459 Log.d(LOG, "Trying to show soft keyboard.");
460 final InputMethodManager manager = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE);
461 manager.showSoftInput(searchText, InputMethodManager.SHOW_IMPLICIT);
464 // ----------------------------------------------------------------
466 // ----------------------------------------------------------------
469 public void onCreateContextMenu(ContextMenu menu, View v,
470 ContextMenuInfo menuInfo) {
471 final int row = getSelectedRow();
476 final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
477 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
478 public boolean onMenuItemClick(MenuItem item) {
479 onAppendToWordList();
484 final MenuItem copy = menu.add(android.R.string.copy);
485 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
486 public boolean onMenuItemClick(MenuItem item) {
494 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
495 final int rowIndex) {
496 Log.d(LOG, "jumpToRow: " + rowIndex);
497 if (dictionaryListAdapter != this.languageList) {
498 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
501 setSelection(rowIndex);
502 setSelectedRow(rowIndex);
503 getListView().setSelected(true);
506 // TODO: delete me somehow.
507 private int getSelectedRow() {
508 return lastSelectedRow;
510 private void setSelectedRow(final int row) {
511 lastSelectedRow = row;
512 Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
516 private void updateSearchText() {
517 Log.d(LOG, "updateSearchText");
518 final int selectedRowIndex = getSelectedRow();
519 if (!searchText.hasFocus()) {
520 if (selectedRowIndex >= 0) {
521 final String word = languageList.languageData
522 .getIndexEntryForRow(selectedRowIndex).word;
523 if (!word.equals(searchText.getText().toString())) {
524 Log.d(LOG, "updateSearchText: setText: " + word);
525 searchText.setText(word);
528 Log.w(LOG, "updateSearchText: nothing selected.");
533 static void startDownloadDictActivity(final Context context) {
534 final Intent intent = new Intent(context, DownloadActivity.class);
535 final SharedPreferences prefs = PreferenceManager
536 .getDefaultSharedPreferences(context);
537 final String dictFetchUrl = prefs.getString(context
538 .getString(R.string.dictFetchUrlKey), context
539 .getString(R.string.dictFetchUrlDefault));
540 final String dictFileName = prefs.getString(context
541 .getString(R.string.dictFileKey), context
542 .getString(R.string.dictFileDefault));
543 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
544 intent.putExtra(DownloadActivity.DEST, dictFileName);
545 context.startActivity(intent);
548 class LanguageListAdapter extends BaseAdapter {
550 // Visible for testing.
551 final LanguageData languageData;
553 LanguageListAdapter(final LanguageData languageData) {
554 this.languageData = languageData;
557 public int getCount() {
558 return languageData.rows.size();
561 public Dictionary.Row getItem(int rowIndex) {
562 assert rowIndex < languageData.rows.size();
563 return languageData.rows.get(rowIndex);
566 public long getItemId(int rowIndex) {
570 public View getView(final int rowIndex, final View convertView,
571 final ViewGroup parent) {
572 final Row row = getItem(rowIndex);
576 TextView result = null;
577 if (convertView instanceof TextView) {
578 result = (TextView) convertView;
580 result = new TextView(parent.getContext());
585 result.setText(languageData.rowToString(row, false));
586 result.setTextAppearance(parent.getContext(),
587 android.R.style.TextAppearance_Large);
588 result.setClickable(false);
593 final TableLayout result = new TableLayout(parent.getContext());
595 final SimpleEntry entry = dictionary.entries.get(row.getIndex());
596 final int rowCount = entry.getRowCount();
597 for (int r = 0; r < rowCount; ++r) {
598 final TableRow tableRow = new TableRow(result.getContext());
600 TextView column1 = new TextView(tableRow.getContext());
601 TextView column2 = new TextView(tableRow.getContext());
602 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
603 layoutParams.weight = 0.5f;
606 final TextView spacer = new TextView(tableRow.getContext());
607 spacer.setText(r == 0 ? "� " : " � ");
608 tableRow.addView(spacer);
610 tableRow.addView(column1, layoutParams);
612 final TextView spacer = new TextView(tableRow.getContext());
613 spacer.setText(r == 0 ? "� " : " � ");
614 tableRow.addView(spacer);
616 tableRow.addView(column2, layoutParams);
620 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
622 // TODO: color words by gender
623 final String col1Text = entry.getAllText(languageData.lang)[r];
624 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
625 final Spannable col1Spannable = (Spannable) column1.getText();
627 final String token = languageData.getIndexEntryForRow(rowIndex).word;
628 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
629 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
630 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
631 startPos += token.length();
635 entry.getAllText(SimpleEntry.otherLang(languageData.lang))[r],
636 TextView.BufferType.NORMAL);
638 result.addView(tableRow);
644 } // DictionaryListAdapter
646 private final class SearchOperation implements Runnable {
647 SearchOperation previousSearchOperation;
649 final LanguageListAdapter listAdapter;
650 final LanguageData languageData;
651 final String searchText;
652 final AtomicBoolean interrupted = new AtomicBoolean(false);
653 boolean searchFinished = false;
655 SearchOperation(final LanguageListAdapter listAdapter,
656 final String searchText, final SearchOperation previousSearchOperation) {
657 this.listAdapter = listAdapter;
658 this.languageData = listAdapter.languageData;
659 this.searchText = searchText;
660 this.previousSearchOperation = previousSearchOperation;
664 if (previousSearchOperation != null) {
665 previousSearchOperation.stopAndWait();
667 previousSearchOperation = null;
669 Log.d(LOG, "SearchOperation: " + searchText);
670 final int indexLocation = languageData.lookup(searchText, interrupted);
671 if (!interrupted.get()) {
672 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
674 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
675 uiHandler.post(new Runnable() {
677 // Check is just a performance operation.
678 if (!interrupted.get()) {
679 // This is safe, because it checks that the listAdapter hasn't changed.
680 jumpToRow(listAdapter, indexEntry.startRow);
682 synchronized (DictionaryActivity.this) {
683 searchOperation = null;
684 DictionaryActivity.this.notifyAll();
689 synchronized (this) {
690 searchFinished = true;
695 private void stopAndWait() {
696 interrupted.set(true);
697 synchronized (this) {
698 while (!searchFinished) {
699 Log.d(LOG, "stopAndWait: " + searchText);
702 } catch (InterruptedException e) {
703 Log.e(LOG, "Interrupted", e);
710 void waitForSearchEnd() {
711 synchronized (this) {
712 while (searchOperation != null) {
713 Log.d(LOG, "waitForSearchEnd");
716 } catch (InterruptedException e) {
717 Log.e(LOG, "Interrupted.", e);
723 private class SearchTextWatcher implements TextWatcher {
724 public void afterTextChanged(final Editable searchTextEditable) {
725 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
726 if (searchText.hasFocus()) {
727 // If they were typing to cause the change, update the UI.
728 onSearchTextChange(searchText.getText().toString());
732 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
736 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {