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;
10 import java.util.concurrent.Executor;
11 import java.util.concurrent.Executors;
12 import java.util.concurrent.atomic.AtomicBoolean;
14 import android.app.ListActivity;
15 import android.content.Context;
16 import android.content.Intent;
17 import android.content.SharedPreferences;
18 import android.content.SharedPreferences.Editor;
19 import android.graphics.Typeface;
20 import android.os.Bundle;
21 import android.os.Handler;
22 import android.preference.PreferenceManager;
23 import android.text.ClipboardManager;
24 import android.text.Editable;
25 import android.text.Spannable;
26 import android.text.TextWatcher;
27 import android.text.style.StyleSpan;
28 import android.util.Log;
29 import android.view.ContextMenu;
30 import android.view.KeyEvent;
31 import android.view.Menu;
32 import android.view.MenuItem;
33 import android.view.View;
34 import android.view.ViewGroup;
35 import android.view.ContextMenu.ContextMenuInfo;
36 import android.view.MenuItem.OnMenuItemClickListener;
37 import android.view.View.OnClickListener;
38 import android.widget.AdapterView;
39 import android.widget.BaseAdapter;
40 import android.widget.Button;
41 import android.widget.EditText;
42 import android.widget.ListView;
43 import android.widget.TableLayout;
44 import android.widget.TableRow;
45 import android.widget.TextView;
46 import android.widget.Toast;
48 import com.hughes.android.dictionary.Dictionary.IndexEntry;
49 import com.hughes.android.dictionary.Dictionary.LanguageData;
50 import com.hughes.android.dictionary.Dictionary.Row;
52 public class DictionaryActivity extends ListActivity {
55 // * Download latest dicts.
56 // * http://ftp.tu-chemnitz.de/pub/Local/urz/ding/de-en-devel/
57 // * http://www1.dict.cc/translation_file_request.php?l=e
58 // * Compress all the strings everywhere, put compression table in file.
60 // * Only one way to way for current search to end. (won't do).
62 static final String LOG = "QuickDic";
63 static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
64 static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
67 final Handler uiHandler = new Handler();
68 private final Executor searchExecutor = Executors.newSingleThreadExecutor();
72 int lastSelectedRow = 0; // TODO: I'm evil.
74 private boolean prefsMightHaveChanged = true;
77 private File wordList;
78 private RandomAccessFile dictRaf = null;
79 private Dictionary dictionary = null;
80 private boolean saveOnlyFirstSubentry = false;
82 // Visible for testing.
83 LanguageListAdapter languageList = null;
84 private SearchOperation searchOperation = null;
86 /** Called when the activity is first created. */
88 public void onCreate(Bundle savedInstanceState) {
89 super.onCreate(savedInstanceState);
90 Log.d(LOG, "onCreate:" + this);
92 if (Language.EN.sortCollator.compare("pre-print", "preppy") >= 0) {
95 Language.EN.sortCollator.getClass() + " is buggy, lookups may not work properly.");
99 initDictionaryAndPrefs();
100 } catch (Exception e) {
106 setContentView(R.layout.main);
107 searchText = (EditText) findViewById(R.id.SearchText);
108 langButton = (Button) findViewById(R.id.LangButton);
110 Log.d(LOG, "adding text changed listener");
111 searchText.addTextChangedListener(new SearchTextWatcher());
113 getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
114 public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
118 public void onNothingSelected(AdapterView<?> arg0) {
122 getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
123 public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
130 final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
131 clearSearchTextButton.setOnClickListener(new OnClickListener() {
132 public void onClick(View v) {
133 clearSearchTextButton.requestFocus();
134 searchText.setText("");
135 searchText.requestFocus();
138 clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
139 getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
142 final Button langButton = (Button) findViewById(R.id.LangButton);
143 langButton.setOnClickListener(new OnClickListener() {
144 public void onClick(View v) {
149 final Button upButton = (Button) findViewById(R.id.UpButton);
150 upButton.setOnClickListener(new OnClickListener() {
151 public void onClick(View v) {
155 final Button downButton = (Button) findViewById(R.id.DownButton);
156 downButton.setOnClickListener(new OnClickListener() {
157 public void onClick(View v) {
163 registerForContextMenu(getListView());
168 private void initDictionaryAndPrefs() throws Exception {
169 if (!prefsMightHaveChanged) {
172 closeCurrentDictionary();
174 final SharedPreferences prefs = PreferenceManager
175 .getDefaultSharedPreferences(this);
176 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
177 getString(R.string.wordListFileDefault)));
178 Log.d(LOG, "wordList=" + wordList);
180 saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
182 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
183 getString(R.string.dictFileDefault)));
184 Log.d(LOG, "dictFile=" + dictFile);
187 if (!dictFile.canRead()) {
188 throw new IOException("Unable to read dictionary file.");
191 dictRaf = new RandomAccessFile(dictFile, "r");
192 dictionary = new Dictionary(dictRaf);
193 } catch (IOException e) {
194 Log.e(LOG, "Couldn't open dictionary.", e);
195 this.startActivity(new Intent(this, NoDictionaryActivity.class));
197 throw new Exception(e);
200 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
203 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
204 setListAdapter(languageList);
205 prefsMightHaveChanged = false;
209 public void onResume() {
211 Log.d(LOG, "onResume:" + this);
214 initDictionaryAndPrefs();
215 } catch (Exception e) {
219 final SharedPreferences prefs = PreferenceManager
220 .getDefaultSharedPreferences(this);
221 final String searchTextString = prefs
222 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
223 searchText.setText(searchTextString);
224 getListView().requestFocus();
225 onSearchTextChange(searchTextString);
229 public void onPause() {
231 Log.d(LOG, "onPause:" + this);
232 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
234 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
235 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
240 public void onStop() {
242 Log.d(LOG, "onStop:" + this);
244 Log.i(LOG, "isFinishing()==true, closing dictionary.");
245 closeCurrentDictionary();
249 private void closeCurrentDictionary() {
250 Log.i(LOG, "closeCurrentDictionary");
253 setListAdapter(null);
256 if (dictRaf != null) {
259 } catch (IOException e) {
260 throw new RuntimeException(e);
265 public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
266 final Row row = languageList.languageData.rows.get(getSelectedRow());
267 return languageList.languageData.rowToString(row, onlyFirstSubentry);
270 // ----------------------------------------------------------------
272 // ----------------------------------------------------------------
274 private MenuItem switchLanguageMenuItem = null;
277 public boolean onCreateOptionsMenu(final Menu menu) {
278 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
279 switchLanguageMenuItem
280 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
281 public boolean onMenuItemClick(final MenuItem menuItem) {
287 final MenuItem preferences = menu.add(getString(R.string.preferences));
288 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
289 public boolean onMenuItemClick(final MenuItem menuItem) {
290 prefsMightHaveChanged = true;
291 startActivity(new Intent(DictionaryActivity.this,
292 PreferenceActivity.class));
297 final MenuItem about = menu.add(getString(R.string.about));
298 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
299 public boolean onMenuItemClick(final MenuItem menuItem) {
300 final Intent intent = new Intent().setClassName(AboutActivity.class
301 .getPackage().getName(), AboutActivity.class.getCanonicalName());
302 final String currentDictInfo;
303 if (dictionary == null) {
304 currentDictInfo = getString(R.string.noDictLoaded);
306 final LanguageData lang0 = dictionary.languageDatas[0];
307 final LanguageData lang1 = dictionary.languageDatas[1];
308 currentDictInfo = getString(R.string.aboutText, dictionary.dictionaryInfo, dictionary.entries.size(),
309 lang0.language.symbol, lang0.sortedIndex.size(), lang0.rows.size(),
310 lang1.language.symbol, lang1.sortedIndex.size(), lang1.rows.size());
312 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
314 startActivity(intent);
319 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
320 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
321 public boolean onMenuItemClick(final MenuItem menuItem) {
322 prefsMightHaveChanged = true;
323 startDownloadDictActivity(DictionaryActivity.this);
332 public boolean onPrepareOptionsMenu(final Menu menu) {
333 switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
334 dictionary.languageDatas[Entry
335 .otherLang(languageList.languageData.lang)].language.symbol));
336 return super.onPrepareOptionsMenu(menu);
339 void updateLangButton() {
340 langButton.setText(languageList.languageData.language.symbol);
343 // ----------------------------------------------------------------
345 // ----------------------------------------------------------------
347 void onLanguageButton() {
349 languageList = new LanguageListAdapter(
350 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
352 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
353 setListAdapter(languageList);
355 onSearchTextChange(searchText.getText().toString());
359 final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
360 Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
361 jumpToRow(languageList, destRowIndex);
364 void onDownButton() {
365 final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
366 Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
367 jumpToRow(languageList, destRowIndex);
370 void onAppendToWordList() {
371 final int row = getSelectedRow();
375 final StringBuilder rawText = new StringBuilder();
376 final String word = languageList.languageData.getIndexEntryForRow(row).word;
378 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
380 rawText.append(word).append("\t");
381 rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
382 Log.d(LOG, "Writing : " + rawText);
384 wordList.getParentFile().mkdirs();
385 final PrintWriter out = new PrintWriter(
386 new FileWriter(wordList, true));
387 out.println(rawText.toString());
389 } catch (IOException e) {
390 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
391 Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
397 final int row = getSelectedRow();
401 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
402 final StringBuilder result = new StringBuilder();
403 result.append(getSelectedRowRawText(false));
404 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
405 clipboardManager.setText(result.toString());
406 Log.d(LOG, "Copied: " + result);
410 public boolean onKeyDown(int keyCode, KeyEvent event) {
411 if (event.getUnicodeChar() != 0) {
412 if (!searchText.hasFocus()) {
413 searchText.setText("" + (char) event.getUnicodeChar());
414 onSearchTextChange(searchText.getText().toString());
415 searchText.requestFocus();
419 return super.onKeyDown(keyCode, event);
423 protected void onListItemClick(ListView l, View v, int row, long id) {
425 openContextMenu(getListView());
428 void onSearchTextChange(final String searchText) {
429 Log.d(LOG, "onSearchTextChange: " + searchText);
430 synchronized (this) {
431 searchOperation = new SearchOperation(languageList, searchText, searchOperation);
432 searchExecutor.execute(searchOperation);
438 // ----------------------------------------------------------------
440 // ----------------------------------------------------------------
443 public void onCreateContextMenu(ContextMenu menu, View v,
444 ContextMenuInfo menuInfo) {
445 final int row = getSelectedRow();
450 final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
451 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
452 public boolean onMenuItemClick(MenuItem item) {
453 onAppendToWordList();
458 final MenuItem copy = menu.add(android.R.string.copy);
459 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
460 public boolean onMenuItemClick(MenuItem item) {
468 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
469 final int rowIndex) {
470 Log.d(LOG, "jumpToRow: " + rowIndex);
471 if (dictionaryListAdapter != this.languageList) {
472 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
475 setSelection(rowIndex);
476 setSelectedRow(rowIndex);
477 getListView().setSelected(true);
480 // TODO: delete me somehow.
481 private int getSelectedRow() {
482 return lastSelectedRow;
484 private void setSelectedRow(final int row) {
485 lastSelectedRow = row;
486 Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
490 private void updateSearchText() {
491 Log.d(LOG, "updateSearchText");
492 final int selectedRowIndex = getSelectedRow();
493 if (!searchText.hasFocus()) {
494 if (selectedRowIndex >= 0) {
495 final String word = languageList.languageData
496 .getIndexEntryForRow(selectedRowIndex).word;
497 if (!word.equals(searchText.getText().toString())) {
498 Log.d(LOG, "updateSearchText: setText: " + word);
499 searchText.setText(word);
502 Log.w(LOG, "updateSearchText: nothing selected.");
507 static void startDownloadDictActivity(final Context context) {
508 final Intent intent = new Intent(context, DownloadActivity.class);
509 final SharedPreferences prefs = PreferenceManager
510 .getDefaultSharedPreferences(context);
511 final String dictFetchUrl = prefs.getString(context
512 .getString(R.string.dictFetchUrlKey), context
513 .getString(R.string.dictFetchUrlDefault));
514 final String dictFileName = prefs.getString(context
515 .getString(R.string.dictFileKey), context
516 .getString(R.string.dictFileDefault));
517 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
518 intent.putExtra(DownloadActivity.DEST, dictFileName);
519 context.startActivity(intent);
522 class LanguageListAdapter extends BaseAdapter {
524 // Visible for testing.
525 final LanguageData languageData;
527 LanguageListAdapter(final LanguageData languageData) {
528 this.languageData = languageData;
531 public int getCount() {
532 return languageData.rows.size();
535 public Dictionary.Row getItem(int rowIndex) {
536 assert rowIndex < languageData.rows.size();
537 return languageData.rows.get(rowIndex);
540 public long getItemId(int rowIndex) {
544 public View getView(final int rowIndex, final View convertView,
545 final ViewGroup parent) {
546 final Row row = getItem(rowIndex);
550 TextView result = null;
551 if (convertView instanceof TextView) {
552 result = (TextView) convertView;
554 result = new TextView(parent.getContext());
559 result.setText(languageData.rowToString(row, false));
560 result.setTextAppearance(parent.getContext(),
561 android.R.style.TextAppearance_Large);
562 result.setClickable(false);
567 final TableLayout result = new TableLayout(parent.getContext());
569 final Entry entry = dictionary.entries.get(row.getIndex());
570 final int rowCount = entry.getRowCount();
571 for (int r = 0; r < rowCount; ++r) {
572 final TableRow tableRow = new TableRow(result.getContext());
574 TextView column1 = new TextView(tableRow.getContext());
575 TextView column2 = new TextView(tableRow.getContext());
576 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
577 layoutParams.weight = 0.5f;
580 final TextView spacer = new TextView(tableRow.getContext());
581 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
582 tableRow.addView(spacer);
584 tableRow.addView(column1, layoutParams);
586 final TextView spacer = new TextView(tableRow.getContext());
587 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
588 tableRow.addView(spacer);
590 tableRow.addView(column2, layoutParams);
594 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
596 // TODO: color words by gender
597 final String col1Text = entry.getAllText(languageData.lang)[r];
598 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
599 final Spannable col1Spannable = (Spannable) column1.getText();
601 final String token = languageData.getIndexEntryForRow(rowIndex).word;
602 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
603 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
604 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
605 startPos += token.length();
609 entry.getAllText(Entry.otherLang(languageData.lang))[r],
610 TextView.BufferType.NORMAL);
612 result.addView(tableRow);
618 } // DictionaryListAdapter
620 private final class SearchOperation implements Runnable {
621 SearchOperation previousSearchOperation;
623 final LanguageListAdapter listAdapter;
624 final LanguageData languageData;
625 final String searchText;
626 final AtomicBoolean interrupted = new AtomicBoolean(false);
627 boolean searchFinished = false;
629 SearchOperation(final LanguageListAdapter listAdapter,
630 final String searchText, final SearchOperation previousSearchOperation) {
631 this.listAdapter = listAdapter;
632 this.languageData = listAdapter.languageData;
633 this.searchText = searchText;
634 this.previousSearchOperation = previousSearchOperation;
638 if (previousSearchOperation != null) {
639 previousSearchOperation.stopAndWait();
641 previousSearchOperation = null;
643 Log.d(LOG, "SearchOperation: " + searchText);
644 final int indexLocation = languageData.lookup(searchText, interrupted);
645 if (!interrupted.get()) {
646 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
648 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
649 uiHandler.post(new Runnable() {
651 // Check is just a performance operation.
652 if (!interrupted.get()) {
653 // This is safe, because it checks that the listAdapter hasn't changed.
654 jumpToRow(listAdapter, indexEntry.startRow);
656 synchronized (DictionaryActivity.this) {
657 searchOperation = null;
658 DictionaryActivity.this.notifyAll();
663 synchronized (this) {
664 searchFinished = true;
669 private void stopAndWait() {
670 interrupted.set(true);
671 synchronized (this) {
672 while (!searchFinished) {
673 Log.d(LOG, "stopAndWait: " + searchText);
676 } catch (InterruptedException e) {
677 Log.e(LOG, "Interrupted", e);
684 void waitForSearchEnd() {
685 synchronized (this) {
686 while (searchOperation != null) {
689 } catch (InterruptedException e) {
690 Log.e(LOG, "Interrupted.", e);
696 private class SearchTextWatcher implements TextWatcher {
697 public void afterTextChanged(final Editable searchTextEditable) {
698 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
699 if (searchText.hasFocus()) {
700 // If they were typing to cause the change, update the UI.
701 onSearchTextChange(searchText.getText().toString());
705 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
709 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {