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.AlertDialog;
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.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;
47 import com.hughes.android.dictionary.Dictionary.IndexEntry;
48 import com.hughes.android.dictionary.Dictionary.LanguageData;
49 import com.hughes.android.dictionary.Dictionary.Row;
51 public class DictionaryActivity extends ListActivity {
54 // * Only have one live SearchActivity, and a way to wait for it to die.
55 // * Don't destroy dict unless we're really shutting down (not on screen rotate).
56 // * Move (re-)init code to a method, set a flag if prefs might have changed, invoke re-init in onResume, which clears flag and reloads prefs.
57 // * Compress all the strings everywhere, put compression table in file.
59 static final String LOG = "QuickDic";
60 static final String PREF_DICT_ACTIVE_LANG = "DICT_DIR_PREF";
61 static final String PREF_ACTIVE_SEARCH_TEXT = "ACTIVE_WORD_PREF";
64 final Handler uiHandler = new Handler();
68 private final Executor searchExecutor = Executors.newSingleThreadExecutor();
71 private boolean prefsMightHaveChanged = true;
72 private File wordList;
74 private RandomAccessFile dictRaf = null;
75 private Dictionary dictionary = null;
77 // Visible for testing.
78 LanguageListAdapter languageList = null;
80 private SearchOperation searchOperation = null;
82 private int selectedRowIndex;
83 private int selectedTokenRowIndex;
85 /** Called when the activity is first created. */
87 public void onCreate(Bundle savedInstanceState) {
88 super.onCreate(savedInstanceState);
89 Log.d(LOG, "onCreate");
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);
107 Log.d(LOG, "adding text changed listener");
108 searchText.addTextChangedListener(new SearchTextWatcher());
111 final Button langButton = (Button) findViewById(R.id.LangButton);
112 langButton.setOnClickListener(new OnClickListener() {
113 public void onClick(View v) {
117 final Button upButton = (Button) findViewById(R.id.UpButton);
118 upButton.setOnClickListener(new OnClickListener() {
119 public void onClick(View v) {
123 final Button downButton = (Button) findViewById(R.id.DownButton);
124 downButton.setOnClickListener(new OnClickListener() {
125 public void onClick(View v) {
131 registerForContextMenu(getListView());
136 private void initDictionaryAndPrefs() {
137 if (!prefsMightHaveChanged) {
140 closeCurrentDictionary();
142 final SharedPreferences prefs = PreferenceManager
143 .getDefaultSharedPreferences(this);
144 wordList = new File(prefs.getString(getString(R.string.wordListFileKey),
145 getString(R.string.wordListFileDefault)));
146 Log.d(LOG, "wordList=" + wordList);
148 final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
149 getString(R.string.dictFileDefault)));
150 Log.d(LOG, "dictFile=" + dictFile);
151 if (!dictFile.canRead()) {
152 Log.w(LOG, "Unable to read dictionary file.");
153 this.startActivity(new Intent(this, NoDictionaryActivity.class));
158 dictRaf = new RandomAccessFile(dictFile, "r");
159 dictionary = new Dictionary(dictRaf);
160 } catch (Exception e) {
161 throw new RuntimeException(e);
164 final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
167 languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
168 setListAdapter(languageList);
169 prefsMightHaveChanged = false;
173 public void onResume() {
176 if (prefsMightHaveChanged) {
180 final SharedPreferences prefs = PreferenceManager
181 .getDefaultSharedPreferences(this);
182 final String searchTextString = prefs
183 .getString(PREF_ACTIVE_SEARCH_TEXT, "");
184 searchText.setText(searchTextString);
185 onSearchTextChange(searchTextString);
189 public void onPause() {
191 final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
193 prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
194 prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
199 public void onStop() {
202 closeCurrentDictionary();
206 private void closeCurrentDictionary() {
207 Log.i(LOG, "closeCurrentDictionary");
208 if (searchOperation != null) {
209 searchOperation.stopAndWait();
210 searchOperation = null;
213 setListAdapter(null);
216 if (dictRaf != null) {
219 } catch (IOException e) {
220 throw new RuntimeException(e);
225 public String getSelectedRowRawText() {
226 final int row = getSelectedItemPosition();
227 return row < 0 ? "" : languageList.languageData
228 .rowToString(languageList.languageData.rows.get(row));
231 // ----------------------------------------------------------------
233 // ----------------------------------------------------------------
235 private MenuItem switchLanguageMenuItem = null;
238 public boolean onCreateOptionsMenu(final Menu menu) {
239 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
240 switchLanguageMenuItem
241 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
242 public boolean onMenuItemClick(final MenuItem menuItem) {
248 final MenuItem preferences = menu.add(getString(R.string.preferences));
249 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
250 public boolean onMenuItemClick(final MenuItem menuItem) {
251 prefsMightHaveChanged = true;
252 startActivity(new Intent(DictionaryActivity.this,
253 PreferenceActivity.class));
258 final MenuItem about = menu.add(getString(R.string.about));
259 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
260 public boolean onMenuItemClick(final MenuItem menuItem) {
261 final Intent intent = new Intent().setClassName(AboutActivity.class
262 .getPackage().getName(), AboutActivity.class.getCanonicalName());
263 final StringBuilder currentDictInfo = new StringBuilder();
264 if (dictionary == null) {
265 currentDictInfo.append(getString(R.string.noDictLoaded));
267 currentDictInfo.append(dictionary.dictionaryInfo).append("\n\n");
268 currentDictInfo.append("Entry count: " + dictionary.entries.size())
270 for (int i = 0; i < 2; ++i) {
271 final LanguageData languageData = dictionary.languageDatas[i];
272 currentDictInfo.append(languageData.language.symbol).append(":\n");
273 currentDictInfo.append(
274 " Unique token count: " + languageData.sortedIndex.size())
276 currentDictInfo.append(" Row count: " + languageData.rows.size())
280 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
282 startActivity(intent);
287 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
288 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
289 public boolean onMenuItemClick(final MenuItem menuItem) {
290 startDownloadDictActivity(DictionaryActivity.this);
299 public boolean onPrepareOptionsMenu(final Menu menu) {
300 switchLanguageMenuItem.setTitle(String.format(
301 getString(R.string.switchToLanguage), dictionary.languageDatas[Entry
302 .otherLang(languageList.languageData.lang)].language.symbol));
303 return super.onPrepareOptionsMenu(menu);
306 void updateLangButton() {
307 final Button langButton = (Button) findViewById(R.id.LangButton);
308 langButton.setText(languageList.languageData.language.symbol);
311 // ----------------------------------------------------------------
313 // ----------------------------------------------------------------
315 void onLanguageButton() {
316 languageList = new LanguageListAdapter(
317 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
319 Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
320 setListAdapter(languageList);
322 onSearchTextChange(searchText.getText().toString());
326 final int destRowIndex;
327 final Row tokenRow = languageList.languageData.rows
328 .get(selectedTokenRowIndex);
329 assert tokenRow.isToken();
330 final int prevTokenIndex = tokenRow.getIndex() - 1;
331 if (selectedRowIndex == selectedTokenRowIndex && selectedRowIndex > 0) {
332 destRowIndex = languageList.languageData.sortedIndex
333 .get(prevTokenIndex).startRow;
334 Log.d(LOG, "onUpButton, jumping back a word, destRowIndex=" + destRowIndex);
336 destRowIndex = selectedTokenRowIndex;
337 Log.d(LOG, "onUpButton, jumping to top of word, destRowIndex=" + destRowIndex);
339 jumpToRow(languageList, destRowIndex);
342 void onDownButton() {
343 final Row tokenRow = languageList.languageData.rows
344 .get(selectedTokenRowIndex);
345 assert tokenRow.isToken();
346 final int nextTokenIndex = tokenRow.getIndex() + 1;
347 final int destRowIndex;
348 if (nextTokenIndex < languageList.languageData.sortedIndex.size()) {
349 destRowIndex = languageList.languageData.sortedIndex
350 .get(nextTokenIndex).startRow;
351 Log.d(LOG, "onDownButton, jumping down a word, destRowIndex=" + destRowIndex);
353 destRowIndex = languageList.languageData.rows.size() - 1;
354 Log.d(LOG, "onDownButton, jumping to end of dict, destRowIndex=" + destRowIndex);
356 jumpToRow(languageList, destRowIndex);
359 void onAppendToWordList() {
360 final int row = getSelectedItemPosition();
364 final StringBuilder rawText = new StringBuilder();
365 final String word = languageList.languageData.getIndexEntryForRow(row).word;
367 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
369 rawText.append(word).append("\t");
370 rawText.append(getSelectedRowRawText());
371 Log.d(LOG, "Writing : " + rawText);
373 wordList.getParentFile().mkdirs();
374 final PrintWriter out = new PrintWriter(
375 new FileWriter(wordList, true));
376 out.println(rawText.toString());
378 } catch (IOException e) {
379 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
380 final AlertDialog alert = new AlertDialog.Builder(
381 DictionaryActivity.this).create();
382 alert.setMessage("Failed to append to file: "
383 + wordList.getAbsolutePath());
390 final int row = getSelectedItemPosition();
394 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedItemPosition());
395 final StringBuilder result = new StringBuilder();
396 result.append(getSelectedRowRawText());
397 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
398 clipboardManager.setText(result.toString());
399 Log.d(LOG, "Copied: " + result);
403 public boolean onKeyDown(int keyCode, KeyEvent event) {
404 if (event.getUnicodeChar() != 0) {
405 if (!searchText.hasFocus()) {
406 searchText.setText("" + (char) event.getUnicodeChar());
407 onSearchTextChange(searchText.getText().toString());
408 searchText.requestFocus();
412 return super.onKeyDown(keyCode, event);
416 protected void onListItemClick(ListView l, View v, int row, long id) {
417 Log.d(LOG, "Clicked: " + getSelectedRowRawText());
418 openContextMenu(getListView());
421 void onSearchTextChange(final String searchText) {
422 Log.d(LOG, "onSearchTextChange: " + searchText);
423 searchOperation = new SearchOperation(languageList, searchText, searchOperation);
424 searchExecutor.execute(searchOperation);
427 // ----------------------------------------------------------------
429 // ----------------------------------------------------------------
432 public void onCreateContextMenu(ContextMenu menu, View v,
433 ContextMenuInfo menuInfo) {
434 final int row = getSelectedItemPosition();
439 final MenuItem addToWordlist = menu.add(String.format(
440 getString(R.string.addToWordList), wordList.getName()));
441 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
442 public boolean onMenuItemClick(MenuItem item) {
443 onAppendToWordList();
448 final MenuItem copy = menu.add(android.R.string.copy);
449 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
450 public boolean onMenuItemClick(MenuItem item) {
458 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
459 final int rowIndex) {
460 Log.d(LOG, "jumpToRow: " + rowIndex);
461 if (dictionaryListAdapter != this.languageList) {
462 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
465 // selectedTokenRowIndex =
466 // languageList.languageData.getIndexEntryForRow(rowIndex).startRow;
467 setSelection(rowIndex);
468 getListView().setSelected(true); // TODO: is this doing anything?
472 private void updateSearchText() {
473 Log.d(LOG, "updateSearchText");
474 if (!searchText.hasFocus()) {
475 final String word = languageList.languageData
476 .getIndexEntryForRow(selectedRowIndex).word;
477 if (!word.equals(searchText.getText().toString())) {
478 Log.d(LOG, "updateSearchText: setText: " + word);
479 searchText.setText(word);
484 static void startDownloadDictActivity(final Context context) {
485 final Intent intent = new Intent(context, DownloadActivity.class);
486 final SharedPreferences prefs = PreferenceManager
487 .getDefaultSharedPreferences(context);
488 final String dictFetchUrl = prefs.getString(context
489 .getString(R.string.dictFetchUrlKey), context
490 .getString(R.string.dictFetchUrlDefault));
491 final String dictFileName = prefs.getString(context
492 .getString(R.string.dictFileKey), context
493 .getString(R.string.dictFileDefault));
494 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
495 intent.putExtra(DownloadActivity.DEST, dictFileName);
496 context.startActivity(intent);
499 class LanguageListAdapter extends BaseAdapter {
501 // Visible for testing.
502 final LanguageData languageData;
504 LanguageListAdapter(final LanguageData languageData) {
505 this.languageData = languageData;
508 public int getCount() {
509 return languageData.rows.size();
512 public Dictionary.Row getItem(int rowIndex) {
513 assert rowIndex < languageData.rows.size();
514 return languageData.rows.get(rowIndex);
517 public long getItemId(int rowIndex) {
521 public View getView(final int rowIndex, final View convertView,
522 final ViewGroup parent) {
523 final Row row = getItem(rowIndex);
527 TextView result = null;
528 if (convertView instanceof TextView) {
529 result = (TextView) convertView;
531 result = new TextView(parent.getContext());
536 result.setText(languageData.rowToString(row));
537 result.setTextAppearance(parent.getContext(),
538 android.R.style.TextAppearance_Large);
539 result.setClickable(false);
544 final TableLayout result = new TableLayout(parent.getContext());
546 final Entry entry = dictionary.entries.get(row.getIndex());
547 final int rowCount = entry.getRowCount();
548 for (int r = 0; r < rowCount; ++r) {
549 final TableRow tableRow = new TableRow(result.getContext());
551 TextView column1 = new TextView(tableRow.getContext());
552 TextView column2 = new TextView(tableRow.getContext());
553 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
554 layoutParams.weight = 0.5f;
557 final TextView spacer = new TextView(tableRow.getContext());
558 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
559 tableRow.addView(spacer);
561 tableRow.addView(column1, layoutParams);
563 final TextView spacer = new TextView(tableRow.getContext());
564 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
565 tableRow.addView(spacer);
567 tableRow.addView(column2, layoutParams);
571 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
573 // TODO: color words by gender
574 final String col1Text = entry.getAllText(languageData.lang)[r];
575 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
576 final Spannable col1Spannable = (Spannable) column1.getText();
578 final String token = languageData.getIndexEntryForRow(rowIndex).word;
579 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
580 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
581 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
582 startPos += token.length();
586 entry.getAllText(Entry.otherLang(languageData.lang))[r],
587 TextView.BufferType.NORMAL);
589 result.addView(tableRow);
595 } // DictionaryListAdapter
597 private final class SearchOperation implements Runnable {
598 SearchOperation previousSearchOperation;
600 final LanguageListAdapter listAdapter;
601 final LanguageData languageData;
602 final String searchText;
603 final AtomicBoolean interrupted = new AtomicBoolean(false);
604 boolean finished = false;
606 SearchOperation(final LanguageListAdapter listAdapter,
607 final String searchText, final SearchOperation previousSearchOperation) {
608 this.listAdapter = listAdapter;
609 this.languageData = listAdapter.languageData;
610 this.searchText = searchText;
611 this.previousSearchOperation = previousSearchOperation;
615 if (previousSearchOperation != null) {
616 previousSearchOperation.stopAndWait();
618 previousSearchOperation = null;
620 Log.d(LOG, "SearchOperation: " + searchText);
621 final int indexLocation = languageData.lookup(searchText, interrupted);
622 if (interrupted.get()) {
625 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
627 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
628 uiHandler.post(new Runnable() {
630 // Check is just a performance operation.
631 if (!interrupted.get()) {
632 // This is safe, because it checks that the listAdapter hasn't changed.
633 jumpToRow(listAdapter, indexEntry.startRow);
638 synchronized (this) {
644 public void stopAndWait() {
645 interrupted.set(true);
646 synchronized (this) {
648 Log.d(LOG, "stopAndWait: " + searchText);
651 } catch (InterruptedException e) {
652 Log.e(LOG, "Interrupted", e);
661 private class SearchTextWatcher implements TextWatcher {
662 public void afterTextChanged(final Editable searchTextEditable) {
663 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
664 if (searchText.hasFocus()) {
665 // If they were typing to cause the change, update the UI.
666 onSearchTextChange(searchText.getText().toString());
670 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
674 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {