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 // * Version magic number at end of dictionary, checked for correctness. (Throw exception in ctor, remember to check for it).
56 // * Localize "about" string if % codes
57 // * Toast when word gets added to word list.
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;
74 private boolean prefsMightHaveChanged = true;
77 private File wordList;
78 private RandomAccessFile dictRaf = null;
79 private Dictionary dictionary = null;
81 // Visible for testing.
82 LanguageListAdapter languageList = null;
83 private SearchOperation searchOperation = null;
85 /** Called when the activity is first created. */
87 public void onCreate(Bundle savedInstanceState) {
88 super.onCreate(savedInstanceState);
89 Log.d(LOG, "onCreate:" + this);
91 if (Language.EN.sortCollator.compare("pre-print", "preppy") >= 0) {
94 "Android java.text.Collator is buggy, lookups may not work properly.");
97 initDictionaryAndPrefs();
98 if (dictRaf == null) {
104 setContentView(R.layout.main);
105 searchText = (EditText) findViewById(R.id.SearchText);
106 langButton = (Button) findViewById(R.id.LangButton);
108 Log.d(LOG, "adding text changed listener");
109 searchText.addTextChangedListener(new SearchTextWatcher());
111 getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
112 public void onItemSelected(AdapterView<?> arg0, View arg1, int arg2,
114 lastSelectedRow = arg2;
117 public void onNothingSelected(AdapterView<?> arg0) {
122 final Button langButton = (Button) findViewById(R.id.LangButton);
123 langButton.setOnClickListener(new OnClickListener() {
124 public void onClick(View v) {
128 final Button upButton = (Button) findViewById(R.id.UpButton);
129 upButton.setOnClickListener(new OnClickListener() {
130 public void onClick(View v) {
134 final Button downButton = (Button) findViewById(R.id.DownButton);
135 downButton.setOnClickListener(new OnClickListener() {
136 public void onClick(View v) {
142 registerForContextMenu(getListView());
147 private void initDictionaryAndPrefs() {
148 if (!prefsMightHaveChanged) {
151 closeCurrentDictionary();
153 final SharedPreferences prefs = PreferenceManager
154 .getDefaultSharedPreferences(this);
155 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
156 getString(R.string.wordListFileDefault)));
157 Log.d(LOG, "wordList=" + wordList);
159 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
160 getString(R.string.dictFileDefault)));
161 Log.d(LOG, "dictFile=" + dictFile);
162 if (!dictFile.canRead()) {
163 Log.w(LOG, "Unable to read dictionary file.");
164 this.startActivity(new Intent(this, NoDictionaryActivity.class));
169 dictRaf = new RandomAccessFile(dictFile, "r");
170 dictionary = new Dictionary(dictRaf);
171 } catch (Exception e) {
172 Log.e(LOG, "Couldn't open dictionary.", e);
176 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
179 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
180 setListAdapter(languageList);
181 prefsMightHaveChanged = false;
185 public void onResume() {
187 Log.d(LOG, "onResume:" + this);
189 initDictionaryAndPrefs();
191 final SharedPreferences prefs = PreferenceManager
192 .getDefaultSharedPreferences(this);
193 final String searchTextString = prefs
194 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
195 searchText.setText(searchTextString);
196 onSearchTextChange(searchTextString);
197 getListView().requestFocus();
201 public void onPause() {
203 Log.d(LOG, "onPause:" + this);
204 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
206 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
207 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
212 public void onStop() {
214 Log.d(LOG, "onStop:" + this);
216 Log.i(LOG, "isFinishing()==true, closing dictionary.");
217 closeCurrentDictionary();
221 private void closeCurrentDictionary() {
222 Log.i(LOG, "closeCurrentDictionary");
225 setListAdapter(null);
228 if (dictRaf != null) {
231 } catch (IOException e) {
232 throw new RuntimeException(e);
237 public String getSelectedRowRawText() {
238 final int row = getSelectedRow();
239 return row < 0 ? "" : languageList.languageData
240 .rowToString(languageList.languageData.rows.get(row));
243 // ----------------------------------------------------------------
245 // ----------------------------------------------------------------
247 private MenuItem switchLanguageMenuItem = null;
250 public boolean onCreateOptionsMenu(final Menu menu) {
251 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
252 switchLanguageMenuItem
253 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
254 public boolean onMenuItemClick(final MenuItem menuItem) {
260 final MenuItem preferences = menu.add(getString(R.string.preferences));
261 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
262 public boolean onMenuItemClick(final MenuItem menuItem) {
263 prefsMightHaveChanged = true;
264 startActivity(new Intent(DictionaryActivity.this,
265 PreferenceActivity.class));
270 final MenuItem about = menu.add(getString(R.string.about));
271 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
272 public boolean onMenuItemClick(final MenuItem menuItem) {
273 final Intent intent = new Intent().setClassName(AboutActivity.class
274 .getPackage().getName(), AboutActivity.class.getCanonicalName());
275 final StringBuilder currentDictInfo = new StringBuilder();
276 if (dictionary == null) {
277 currentDictInfo.append(getString(R.string.noDictLoaded));
279 currentDictInfo.append(dictionary.dictionaryInfo).append("\n\n");
280 currentDictInfo.append("Entry count: " + dictionary.entries.size())
282 for (int i = 0; i < 2; ++i) {
283 final LanguageData languageData = dictionary.languageDatas[i];
284 currentDictInfo.append(languageData.language.symbol).append(":\n");
285 currentDictInfo.append(
286 " Unique token count: " + languageData.sortedIndex.size())
288 currentDictInfo.append(" Row count: " + languageData.rows.size())
292 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
294 startActivity(intent);
299 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
300 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
301 public boolean onMenuItemClick(final MenuItem menuItem) {
302 startDownloadDictActivity(DictionaryActivity.this);
311 public boolean onPrepareOptionsMenu(final Menu menu) {
312 switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
313 dictionary.languageDatas[Entry
314 .otherLang(languageList.languageData.lang)].language.symbol));
315 return super.onPrepareOptionsMenu(menu);
318 void updateLangButton() {
319 langButton.setText(languageList.languageData.language.symbol);
322 // ----------------------------------------------------------------
324 // ----------------------------------------------------------------
326 void onLanguageButton() {
328 languageList = new LanguageListAdapter(
329 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
331 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
332 setListAdapter(languageList);
334 onSearchTextChange(searchText.getText().toString());
338 final int destRowIndex = languageList.languageData.getPrevTokenRow(lastSelectedRow);
339 Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
340 jumpToRow(languageList, destRowIndex);
343 void onDownButton() {
344 final int destRowIndex = languageList.languageData.getNextTokenRow(lastSelectedRow);
345 Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
346 jumpToRow(languageList, destRowIndex);
349 void onAppendToWordList() {
350 final int row = getSelectedRow();
354 final StringBuilder rawText = new StringBuilder();
355 final String word = languageList.languageData.getIndexEntryForRow(row).word;
357 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
359 rawText.append(word).append("\t");
360 rawText.append(getSelectedRowRawText());
361 Log.d(LOG, "Writing : " + rawText);
363 wordList.getParentFile().mkdirs();
364 final PrintWriter out = new PrintWriter(
365 new FileWriter(wordList, true));
366 out.println(rawText.toString());
368 } catch (IOException e) {
369 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
370 Toast.makeText(this, getString(R.string.failedAddingToWordList, wordList.getAbsolutePath()), Toast.LENGTH_LONG);
376 final int row = getSelectedRow();
380 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedRow());
381 final StringBuilder result = new StringBuilder();
382 result.append(getSelectedRowRawText());
383 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
384 clipboardManager.setText(result.toString());
385 Log.d(LOG, "Copied: " + result);
389 public boolean onKeyDown(int keyCode, KeyEvent event) {
390 if (event.getUnicodeChar() != 0) {
391 if (!searchText.hasFocus()) {
392 searchText.setText("" + (char) event.getUnicodeChar());
393 onSearchTextChange(searchText.getText().toString());
394 searchText.requestFocus();
398 return super.onKeyDown(keyCode, event);
402 protected void onListItemClick(ListView l, View v, int row, long id) {
403 Log.d(LOG, "Clicked: " + getSelectedRowRawText());
404 openContextMenu(getListView());
407 void onSearchTextChange(final String searchText) {
408 Log.d(LOG, "onSearchTextChange: " + searchText);
409 synchronized (this) {
410 searchOperation = new SearchOperation(languageList, searchText, searchOperation);
411 searchExecutor.execute(searchOperation);
417 // ----------------------------------------------------------------
419 // ----------------------------------------------------------------
422 public void onCreateContextMenu(ContextMenu menu, View v,
423 ContextMenuInfo menuInfo) {
424 final int row = getSelectedRow();
429 final MenuItem addToWordlist = menu.add(getString(R.string.addToWordList, wordList.getName()));
430 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
431 public boolean onMenuItemClick(MenuItem item) {
432 onAppendToWordList();
437 final MenuItem copy = menu.add(android.R.string.copy);
438 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
439 public boolean onMenuItemClick(MenuItem item) {
447 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
448 final int rowIndex) {
449 Log.d(LOG, "jumpToRow: " + rowIndex);
450 if (dictionaryListAdapter != this.languageList) {
451 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
454 setSelection(rowIndex);
455 lastSelectedRow = rowIndex; // TODO: find a way to delete this.
457 getListView().setSelected(true);
460 // TODO: delete me somehow.
461 private int getSelectedRow() {
462 return lastSelectedRow;
465 private void updateSearchText() {
466 Log.d(LOG, "updateSearchText");
467 final int selectedRowIndex = getSelectedRow();
468 if (!searchText.hasFocus()) {
469 if (selectedRowIndex >= 0) {
470 final String word = languageList.languageData
471 .getIndexEntryForRow(selectedRowIndex).word;
472 if (!word.equals(searchText.getText().toString())) {
473 Log.d(LOG, "updateSearchText: setText: " + word);
474 searchText.setText(word);
477 Log.w(LOG, "updateSearchText: nothing selected.");
482 static void startDownloadDictActivity(final Context context) {
483 final Intent intent = new Intent(context, DownloadActivity.class);
484 final SharedPreferences prefs = PreferenceManager
485 .getDefaultSharedPreferences(context);
486 final String dictFetchUrl = prefs.getString(context
487 .getString(R.string.dictFetchUrlKey), context
488 .getString(R.string.dictFetchUrlDefault));
489 final String dictFileName = prefs.getString(context
490 .getString(R.string.dictFileKey), context
491 .getString(R.string.dictFileDefault));
492 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
493 intent.putExtra(DownloadActivity.DEST, dictFileName);
494 context.startActivity(intent);
497 class LanguageListAdapter extends BaseAdapter {
499 // Visible for testing.
500 final LanguageData languageData;
502 LanguageListAdapter(final LanguageData languageData) {
503 this.languageData = languageData;
506 public int getCount() {
507 return languageData.rows.size();
510 public Dictionary.Row getItem(int rowIndex) {
511 assert rowIndex < languageData.rows.size();
512 return languageData.rows.get(rowIndex);
515 public long getItemId(int rowIndex) {
519 public View getView(final int rowIndex, final View convertView,
520 final ViewGroup parent) {
521 final Row row = getItem(rowIndex);
525 TextView result = null;
526 if (convertView instanceof TextView) {
527 result = (TextView) convertView;
529 result = new TextView(parent.getContext());
534 result.setText(languageData.rowToString(row));
535 result.setTextAppearance(parent.getContext(),
536 android.R.style.TextAppearance_Large);
537 result.setClickable(false);
542 final TableLayout result = new TableLayout(parent.getContext());
544 final Entry entry = dictionary.entries.get(row.getIndex());
545 final int rowCount = entry.getRowCount();
546 for (int r = 0; r < rowCount; ++r) {
547 final TableRow tableRow = new TableRow(result.getContext());
549 TextView column1 = new TextView(tableRow.getContext());
550 TextView column2 = new TextView(tableRow.getContext());
551 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
552 layoutParams.weight = 0.5f;
555 final TextView spacer = new TextView(tableRow.getContext());
556 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
557 tableRow.addView(spacer);
559 tableRow.addView(column1, layoutParams);
561 final TextView spacer = new TextView(tableRow.getContext());
562 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
563 tableRow.addView(spacer);
565 tableRow.addView(column2, layoutParams);
569 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
571 // TODO: color words by gender
572 final String col1Text = entry.getAllText(languageData.lang)[r];
573 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
574 final Spannable col1Spannable = (Spannable) column1.getText();
576 final String token = languageData.getIndexEntryForRow(rowIndex).word;
577 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
578 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
579 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
580 startPos += token.length();
584 entry.getAllText(Entry.otherLang(languageData.lang))[r],
585 TextView.BufferType.NORMAL);
587 result.addView(tableRow);
593 } // DictionaryListAdapter
595 private final class SearchOperation implements Runnable {
596 SearchOperation previousSearchOperation;
598 final LanguageListAdapter listAdapter;
599 final LanguageData languageData;
600 final String searchText;
601 final AtomicBoolean interrupted = new AtomicBoolean(false);
602 boolean searchFinished = false;
604 SearchOperation(final LanguageListAdapter listAdapter,
605 final String searchText, final SearchOperation previousSearchOperation) {
606 this.listAdapter = listAdapter;
607 this.languageData = listAdapter.languageData;
608 this.searchText = searchText;
609 this.previousSearchOperation = previousSearchOperation;
613 if (previousSearchOperation != null) {
614 previousSearchOperation.stopAndWait();
616 previousSearchOperation = null;
618 Log.d(LOG, "SearchOperation: " + searchText);
619 final int indexLocation = languageData.lookup(searchText, interrupted);
620 if (!interrupted.get()) {
621 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
623 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
624 uiHandler.post(new Runnable() {
626 // Check is just a performance operation.
627 if (!interrupted.get()) {
628 // This is safe, because it checks that the listAdapter hasn't changed.
629 jumpToRow(listAdapter, indexEntry.startRow);
631 synchronized (DictionaryActivity.this) {
632 searchOperation = null;
633 DictionaryActivity.this.notifyAll();
638 synchronized (this) {
639 searchFinished = true;
644 private void stopAndWait() {
645 interrupted.set(true);
646 synchronized (this) {
647 while (!searchFinished) {
648 Log.d(LOG, "stopAndWait: " + searchText);
651 } catch (InterruptedException e) {
652 Log.e(LOG, "Interrupted", e);
659 void waitForSearchEnd() {
660 synchronized (this) {
661 while (searchOperation != null) {
664 } catch (InterruptedException e) {
665 Log.e(LOG, "Interrupted.", e);
671 private class SearchTextWatcher implements TextWatcher {
672 public void afterTextChanged(final Editable searchTextEditable) {
673 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
674 if (searchText.hasFocus()) {
675 // If they were typing to cause the change, update the UI.
676 onSearchTextChange(searchText.getText().toString());
680 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
684 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {