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 searchOperation.stopAndWait();
210 setListAdapter(null);
213 if (dictRaf != null) {
216 } catch (IOException e) {
217 throw new RuntimeException(e);
222 public String getSelectedRowRawText() {
223 final int row = getSelectedItemPosition();
224 return row < 0 ? "" : languageList.languageData
225 .rowToString(languageList.languageData.rows.get(row));
228 // ----------------------------------------------------------------
230 // ----------------------------------------------------------------
232 private MenuItem switchLanguageMenuItem = null;
235 public boolean onCreateOptionsMenu(final Menu menu) {
236 switchLanguageMenuItem = menu.add(getString(R.string.switchToLanguage));
237 switchLanguageMenuItem
238 .setOnMenuItemClickListener(new OnMenuItemClickListener() {
239 public boolean onMenuItemClick(final MenuItem menuItem) {
245 final MenuItem preferences = menu.add(getString(R.string.preferences));
246 preferences.setOnMenuItemClickListener(new OnMenuItemClickListener() {
247 public boolean onMenuItemClick(final MenuItem menuItem) {
248 prefsMightHaveChanged = true;
249 startActivity(new Intent(DictionaryActivity.this,
250 PreferenceActivity.class));
255 final MenuItem about = menu.add(getString(R.string.about));
256 about.setOnMenuItemClickListener(new OnMenuItemClickListener() {
257 public boolean onMenuItemClick(final MenuItem menuItem) {
258 final Intent intent = new Intent().setClassName(AboutActivity.class
259 .getPackage().getName(), AboutActivity.class.getCanonicalName());
260 final StringBuilder currentDictInfo = new StringBuilder();
261 if (dictionary == null) {
262 currentDictInfo.append(getString(R.string.noDictLoaded));
264 currentDictInfo.append(dictionary.dictionaryInfo).append("\n\n");
265 currentDictInfo.append("Entry count: " + dictionary.entries.size())
267 for (int i = 0; i < 2; ++i) {
268 final LanguageData languageData = dictionary.languageDatas[i];
269 currentDictInfo.append(languageData.language.symbol).append(":\n");
270 currentDictInfo.append(
271 " Unique token count: " + languageData.sortedIndex.size())
273 currentDictInfo.append(" Row count: " + languageData.rows.size())
277 intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
279 startActivity(intent);
284 final MenuItem download = menu.add(getString(R.string.downloadDictionary));
285 download.setOnMenuItemClickListener(new OnMenuItemClickListener() {
286 public boolean onMenuItemClick(final MenuItem menuItem) {
287 startDownloadDictActivity(DictionaryActivity.this);
296 public boolean onPrepareOptionsMenu(final Menu menu) {
297 switchLanguageMenuItem.setTitle(String.format(
298 getString(R.string.switchToLanguage), dictionary.languageDatas[Entry
299 .otherLang(languageList.languageData.lang)].language.symbol));
300 return super.onPrepareOptionsMenu(menu);
303 void updateLangButton() {
304 final Button langButton = (Button) findViewById(R.id.LangButton);
305 langButton.setText(languageList.languageData.language.symbol);
308 // ----------------------------------------------------------------
310 // ----------------------------------------------------------------
312 void onLanguageButton() {
313 languageList = new LanguageListAdapter(
314 dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
316 setListAdapter(languageList);
318 onSearchTextChange(searchText.getText().toString());
322 final int destRowIndex;
323 final Row tokenRow = languageList.languageData.rows
324 .get(selectedTokenRowIndex);
325 assert tokenRow.isToken();
326 final int prevTokenIndex = tokenRow.getIndex() - 1;
327 if (selectedRowIndex == selectedTokenRowIndex && selectedRowIndex > 0) {
328 destRowIndex = languageList.languageData.sortedIndex
329 .get(prevTokenIndex).startRow;
331 destRowIndex = selectedTokenRowIndex;
333 jumpToRow(languageList, destRowIndex);
336 void onDownButton() {
337 final Row tokenRow = languageList.languageData.rows
338 .get(selectedTokenRowIndex);
339 assert tokenRow.isToken();
340 final int nextTokenIndex = tokenRow.getIndex() + 1;
341 final int destRowIndex;
342 if (nextTokenIndex < languageList.languageData.sortedIndex.size()) {
343 destRowIndex = languageList.languageData.sortedIndex
344 .get(nextTokenIndex).startRow;
346 destRowIndex = languageList.languageData.rows.size() - 1;
348 jumpToRow(languageList, destRowIndex);
351 void onAppendToWordList() {
352 final int row = getSelectedItemPosition();
356 final StringBuilder rawText = new StringBuilder();
357 final String word = languageList.languageData.getIndexEntryForRow(row).word;
359 new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
361 rawText.append(word).append("\t");
362 rawText.append(getSelectedRowRawText());
363 Log.d(LOG, "Writing : " + rawText);
365 wordList.getParentFile().mkdirs();
366 final PrintWriter out = new PrintWriter(
367 new FileWriter(wordList, true));
368 out.println(rawText.toString());
370 } catch (IOException e) {
371 Log.e(LOG, "Unable to append to " + wordList.getAbsolutePath(), e);
372 final AlertDialog alert = new AlertDialog.Builder(
373 DictionaryActivity.this).create();
374 alert.setMessage("Failed to append to file: "
375 + wordList.getAbsolutePath());
382 final int row = getSelectedItemPosition();
386 Log.d(LOG, "Copy." + DictionaryActivity.this.getSelectedItemPosition());
387 final StringBuilder result = new StringBuilder();
388 result.append(getSelectedRowRawText());
389 final ClipboardManager clipboardManager = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE);
390 clipboardManager.setText(result.toString());
391 Log.d(LOG, "Copied: " + result);
395 public boolean onKeyDown(int keyCode, KeyEvent event) {
396 if (event.getUnicodeChar() != 0) {
397 if (!searchText.hasFocus()) {
398 searchText.setText("" + (char) event.getUnicodeChar());
399 onSearchTextChange(searchText.getText().toString());
400 searchText.requestFocus();
404 return super.onKeyDown(keyCode, event);
408 protected void onListItemClick(ListView l, View v, int row, long id) {
409 Log.d(LOG, "Clicked: " + getSelectedRowRawText());
410 openContextMenu(getListView());
413 void onSearchTextChange(final String searchText) {
414 Log.d(LOG, "onSearchTextChange: " + searchText);
415 searchOperation = new SearchOperation(languageList, searchText, searchOperation);
416 searchExecutor.execute(searchOperation);
419 // ----------------------------------------------------------------
421 // ----------------------------------------------------------------
424 public void onCreateContextMenu(ContextMenu menu, View v,
425 ContextMenuInfo menuInfo) {
426 final int row = getSelectedItemPosition();
431 final MenuItem addToWordlist = menu.add(String.format(
432 getString(R.string.addToWordList), wordList.getName()));
433 addToWordlist.setOnMenuItemClickListener(new OnMenuItemClickListener() {
434 public boolean onMenuItemClick(MenuItem item) {
435 onAppendToWordList();
440 final MenuItem copy = menu.add(android.R.string.copy);
441 copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
442 public boolean onMenuItemClick(MenuItem item) {
450 private void jumpToRow(final LanguageListAdapter dictionaryListAdapter,
451 final int rowIndex) {
452 Log.d(LOG, "jumpToRow: " + rowIndex);
453 if (dictionaryListAdapter != this.languageList) {
454 Log.w(LOG, "skipping jumpToRow for old list adapter: " + rowIndex);
457 // selectedTokenRowIndex =
458 // languageList.languageData.getIndexEntryForRow(rowIndex).startRow;
459 setSelection(rowIndex);
460 getListView().setSelected(true); // TODO: is this doing anything?
464 private void updateSearchText() {
465 Log.d(LOG, "updateSearchText");
466 if (!searchText.hasFocus()) {
467 final String word = languageList.languageData
468 .getIndexEntryForRow(selectedRowIndex).word;
469 if (!word.equals(searchText.getText().toString())) {
470 Log.d(LOG, "updateSearchText: setText: " + word);
471 searchText.setText(word);
476 static void startDownloadDictActivity(final Context context) {
477 final Intent intent = new Intent(context, DownloadActivity.class);
478 final SharedPreferences prefs = PreferenceManager
479 .getDefaultSharedPreferences(context);
480 final String dictFetchUrl = prefs.getString(context
481 .getString(R.string.dictFetchUrlKey), context
482 .getString(R.string.dictFetchUrlDefault));
483 final String dictFileName = prefs.getString(context
484 .getString(R.string.dictFileKey), context
485 .getString(R.string.dictFileDefault));
486 intent.putExtra(DownloadActivity.SOURCE, dictFetchUrl);
487 intent.putExtra(DownloadActivity.DEST, dictFileName);
488 context.startActivity(intent);
491 class LanguageListAdapter extends BaseAdapter {
493 // Visible for testing.
494 final LanguageData languageData;
496 LanguageListAdapter(final LanguageData languageData) {
497 this.languageData = languageData;
500 public int getCount() {
501 return languageData.rows.size();
504 public Dictionary.Row getItem(int rowIndex) {
505 assert rowIndex < languageData.rows.size();
506 return languageData.rows.get(rowIndex);
509 public long getItemId(int rowIndex) {
513 public View getView(final int rowIndex, final View convertView,
514 final ViewGroup parent) {
515 final Row row = getItem(rowIndex);
519 TextView result = null;
520 if (convertView instanceof TextView) {
521 result = (TextView) convertView;
523 result = new TextView(parent.getContext());
528 result.setText(languageData.rowToString(row));
529 result.setTextAppearance(parent.getContext(),
530 android.R.style.TextAppearance_Large);
531 result.setClickable(false);
536 final TableLayout result = new TableLayout(parent.getContext());
538 final Entry entry = dictionary.entries.get(row.getIndex());
539 final int rowCount = entry.getRowCount();
540 for (int r = 0; r < rowCount; ++r) {
541 final TableRow tableRow = new TableRow(result.getContext());
543 TextView column1 = new TextView(tableRow.getContext());
544 TextView column2 = new TextView(tableRow.getContext());
545 final TableRow.LayoutParams layoutParams = new TableRow.LayoutParams();
546 layoutParams.weight = 0.5f;
549 final TextView spacer = new TextView(tableRow.getContext());
550 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
551 tableRow.addView(spacer);
553 tableRow.addView(column1, layoutParams);
555 final TextView spacer = new TextView(tableRow.getContext());
556 spacer.setText(r == 0 ? "
\95 " : "
\95 ");
557 tableRow.addView(spacer);
559 tableRow.addView(column2, layoutParams);
563 // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
565 // TODO: color words by gender
566 final String col1Text = entry.getAllText(languageData.lang)[r];
567 column1.setText(col1Text, TextView.BufferType.SPANNABLE);
568 final Spannable col1Spannable = (Spannable) column1.getText();
570 final String token = languageData.getIndexEntryForRow(rowIndex).word;
571 while ((startPos = col1Text.indexOf(token, startPos)) != -1) {
572 col1Spannable.setSpan(new StyleSpan(Typeface.BOLD), startPos,
573 startPos + token.length(), Spannable.SPAN_INCLUSIVE_EXCLUSIVE);
574 startPos += token.length();
578 entry.getAllText(Entry.otherLang(languageData.lang))[r],
579 TextView.BufferType.NORMAL);
581 result.addView(tableRow);
587 } // DictionaryListAdapter
589 private final class SearchOperation implements Runnable {
590 SearchOperation previousSearchOperation;
592 final LanguageListAdapter listAdapter;
593 final LanguageData languageData;
594 final String searchText;
595 final AtomicBoolean interrupted = new AtomicBoolean(false);
596 boolean finished = false;
598 SearchOperation(final LanguageListAdapter listAdapter,
599 final String searchText, final SearchOperation previousSearchOperation) {
600 this.listAdapter = listAdapter;
601 this.languageData = listAdapter.languageData;
602 this.searchText = searchText;
603 this.previousSearchOperation = previousSearchOperation;
607 if (previousSearchOperation != null) {
608 previousSearchOperation.stopAndWait();
610 previousSearchOperation = null;
612 Log.d(LOG, "SearchOperation: " + searchText);
613 final int indexLocation = languageData.lookup(searchText, interrupted);
614 if (interrupted.get()) {
617 final IndexEntry indexEntry = languageData.sortedIndex.get(indexLocation);
619 Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
620 uiHandler.post(new Runnable() {
622 // Check is just a performance operation.
623 if (!interrupted.get()) {
624 // This is safe, because it checks that the listAdapter hasn't changed.
625 jumpToRow(listAdapter, indexEntry.startRow);
630 synchronized (this) {
636 public void stopAndWait() {
637 interrupted.set(true);
638 synchronized (this) {
640 Log.d(LOG, "stopAndWait: " + searchText);
643 } catch (InterruptedException e) {
644 Log.e(LOG, "Interrupted", e);
653 private class SearchTextWatcher implements TextWatcher {
654 public void afterTextChanged(final Editable searchTextEditable) {
655 Log.d(LOG, "Search text changed: " + searchText.getText().toString());
656 if (searchText.hasFocus()) {
657 // If they were typing to cause the change, update the UI.
658 onSearchTextChange(searchText.getText().toString());
662 public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
666 public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {