]> gitweb.fperrin.net Git - Dictionary.git/blob - src/com/hughes/android/dictionary/DictionaryActivity.java
05f292a9a2e4729aec74dc375eff39bcf1612563
[Dictionary.git] / src / com / hughes / android / dictionary / DictionaryActivity.java
1 package com.hughes.android.dictionary;
2
3 import java.io.File;
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;
14
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;
49
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;
54
55 public class DictionaryActivity extends ListActivity {
56   
57   // TO DO:
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.
62   // Done:
63   // * Only one way to way for current search to end. (won't do).
64
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";
68
69   // package for test.
70   final Handler uiHandler = new Handler();
71   private final Executor searchExecutor = Executors.newSingleThreadExecutor();
72
73   EditText searchText;
74   Button langButton;
75   int lastSelectedRow = 0;  // TODO: I'm evil.
76
77   private boolean prefsMightHaveChanged = true;
78
79   // Never null.
80   private File wordList;
81   private RandomAccessFile dictRaf = null;
82   private Dictionary dictionary = null;
83   private boolean saveOnlyFirstSubentry = false;
84
85   // Visible for testing.
86   LanguageListAdapter languageList = null;
87   private SearchOperation searchOperation = null;
88   
89   public DictionaryActivity() {
90
91     searchExecutor.execute(new Runnable() {
92       public void run() {
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.");
99           }
100         }
101         Log.d(LOG, "Loading collators took:" + (System.currentTimeMillis() - startMillis));
102       }
103     });
104
105   }
106
107   /** Called when the activity is first created. */
108   @Override
109   public void onCreate(Bundle savedInstanceState) {
110     super.onCreate(savedInstanceState);
111     Log.d(LOG, "onCreate:" + this);
112
113     try {
114       initDictionaryAndPrefs();
115     } catch (Exception e) {
116       return;
117     }
118
119     // UI init.
120
121     setContentView(R.layout.main);
122     searchText = (EditText) findViewById(R.id.SearchText);
123     langButton = (Button) findViewById(R.id.LangButton);
124     
125     Log.d(LOG, "adding text changed listener");
126     searchText.addTextChangedListener(new SearchTextWatcher());
127     
128     getListView().setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
129       public void onItemSelected(AdapterView<?> arg0, View arg1, int row,
130           long arg3) {
131         setSelectedRow(row);
132       }
133       public void onNothingSelected(AdapterView<?> arg0) {
134       }
135     });
136     
137     getListView().setOnItemLongClickListener(new AdapterView.OnItemLongClickListener() {
138       public boolean onItemLongClick(AdapterView<?> arg0, View arg1, int row,
139           long arg3) {
140         setSelectedRow(row);
141         return false;
142       }
143     });
144     
145     final Button clearSearchTextButton = (Button) findViewById(R.id.ClearSearchTextButton);
146     clearSearchTextButton.setOnClickListener(new OnClickListener() {
147       public void onClick(View v) {
148         onClearSearchTextButton(clearSearchTextButton);
149       }
150     });
151     clearSearchTextButton.setVisibility(PreferenceManager.getDefaultSharedPreferences(this).getBoolean(
152         getString(R.string.showClearSearchTextButtonKey), true) ? View.VISIBLE
153         : View.GONE);
154     
155     final Button langButton = (Button) findViewById(R.id.LangButton);
156     langButton.setOnClickListener(new OnClickListener() {
157       public void onClick(View v) {
158         onLanguageButton();
159       }
160     });
161     
162     final Button upButton = (Button) findViewById(R.id.UpButton);
163     upButton.setOnClickListener(new OnClickListener() {
164       public void onClick(View v) {
165         onUpButton();
166       }
167     });
168     final Button downButton = (Button) findViewById(R.id.DownButton);
169     downButton.setOnClickListener(new OnClickListener() {
170       public void onClick(View v) {
171         onDownButton();
172       }
173     });
174
175     // ContextMenu.
176     registerForContextMenu(getListView());
177
178     updateLangButton();
179   }
180   
181   private void initDictionaryAndPrefs() throws Exception {
182     if (!prefsMightHaveChanged) {
183       return;
184     }
185     closeCurrentDictionary();
186     
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);
192     
193     saveOnlyFirstSubentry = prefs.getBoolean(getString(R.string.saveOnlyFirstSubentryKey), false);
194
195     final File dictFile = new File(prefs.getString(getString(R.string.dictFileKey),
196         getString(R.string.dictFileDefault)));
197     Log.d(LOG, "dictFile=" + dictFile);
198     
199     try {
200       if (!dictFile.canRead()) {
201         throw new IOException("Unable to read dictionary file.");
202       }
203       
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));
211       finish();
212       throw new Exception(e);
213     }
214     
215     final byte lang = prefs.getInt(PREF_DICT_ACTIVE_LANG, Entry.LANG1) == Entry.LANG1 ? Entry.LANG1
216         : Entry.LANG2;
217     
218     languageList = new LanguageListAdapter(dictionary.languageDatas[lang]);
219     setListAdapter(languageList);
220     prefsMightHaveChanged = false;
221   }
222
223   @Override
224   public void onResume() {
225     super.onResume();
226     Log.d(LOG, "onResume:" + this);
227
228     try {
229       initDictionaryAndPrefs();
230     } catch (Exception e) {
231       return;
232     }
233     
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);
241   }
242
243   @Override
244   public void onPause() {
245     super.onPause();
246     Log.d(LOG, "onPause:" + this);
247     final Editor prefs = PreferenceManager.getDefaultSharedPreferences(this)
248         .edit();
249     prefs.putInt(PREF_DICT_ACTIVE_LANG, languageList.languageData.lang);
250     prefs.putString(PREF_ACTIVE_SEARCH_TEXT, searchText.getText().toString());
251     prefs.commit();
252   }
253
254   @Override
255   public void onStop() {
256     super.onStop();
257     Log.d(LOG, "onStop:" + this);
258     if (isFinishing()) {
259       Log.i(LOG, "isFinishing()==true, closing dictionary.");
260       closeCurrentDictionary();
261     }
262   }
263
264   private void closeCurrentDictionary() {
265     Log.i(LOG, "closeCurrentDictionary");
266     if (dictionary == null) {
267       return;
268     }
269     waitForSearchEnd();
270     languageList = null;
271     setListAdapter(null);
272     Log.d(LOG, "setListAdapter finished.");
273     dictionary = null;
274     try {
275       if (dictRaf != null) {
276         dictRaf.close();
277       }
278     } catch (IOException e) {
279       throw new RuntimeException(e);
280     }
281     dictRaf = null;
282   }
283
284   public String getSelectedRowRawText(final boolean onlyFirstSubentry) {
285     final Row row = languageList.languageData.rows.get(getSelectedRow());
286     return languageList.languageData.rowToString(row, onlyFirstSubentry);
287   }
288
289   // ----------------------------------------------------------------
290   // OptionsMenu
291   // ----------------------------------------------------------------
292
293   private MenuItem switchLanguageMenuItem = null;
294
295   @Override
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) {
301             onLanguageButton();
302             return false;
303           }
304         });
305
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));
312         return false;
313       }
314     });
315
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);
324         } else {
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());
330         }
331         intent.putExtra(AboutActivity.CURRENT_DICT_INFO, currentDictInfo
332             .toString());
333         startActivity(intent);
334         return false;
335       }
336     });
337
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);
343         return false;
344       }
345     });
346
347     return true;
348   }
349
350   @Override
351   public boolean onPrepareOptionsMenu(final Menu menu) {
352     switchLanguageMenuItem.setTitle(getString(R.string.switchToLanguage,
353         dictionary.languageDatas[Entry
354             .otherLang(languageList.languageData.lang)].language.symbol));
355     return super.onPrepareOptionsMenu(menu);
356   }
357   
358   void updateLangButton() {
359     langButton.setText(languageList.languageData.language.symbol);
360   }
361
362   // ----------------------------------------------------------------
363   // Event handlers.
364   // ----------------------------------------------------------------
365   
366   void onLanguageButton() {
367     waitForSearchEnd();
368     languageList = new LanguageListAdapter(
369         dictionary.languageDatas[(languageList.languageData == dictionary.languageDatas[0]) ? 1
370             : 0]);
371     Log.d(LOG, "onLanguageButton, newLang=" + languageList.languageData.language.symbol);
372     setListAdapter(languageList);
373     updateLangButton();
374     onSearchTextChange(searchText.getText().toString());
375   }
376
377   void onUpButton() {
378     final int destRowIndex = languageList.languageData.getPrevTokenRow(getSelectedRow());
379     Log.d(LOG, "onUpButton, destRowIndex=" + destRowIndex);
380     jumpToRow(languageList, destRowIndex);
381   }
382
383   void onDownButton() {
384     final int destRowIndex = languageList.languageData.getNextTokenRow(getSelectedRow());
385     Log.d(LOG, "onDownButton, destRowIndex=" + destRowIndex);
386     jumpToRow(languageList, destRowIndex);
387   }
388
389   void onAppendToWordList() {
390     final int row = getSelectedRow();
391     if (row < 0) {
392       return;
393     }
394     final StringBuilder rawText = new StringBuilder();
395     final String word = languageList.languageData.getIndexEntryForRow(row).word;
396     rawText.append(
397         new SimpleDateFormat("yyyy.MM.dd HH:mm:ss").format(new Date()))
398         .append("\t");
399     rawText.append(word).append("\t");
400     rawText.append(getSelectedRowRawText(saveOnlyFirstSubentry));
401     Log.d(LOG, "Writing : " + rawText);
402     try {
403       wordList.getParentFile().mkdirs();
404       final PrintWriter out = new PrintWriter(
405           new FileWriter(wordList, true));
406       out.println(rawText.toString());
407       out.close();
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);
411     }
412     return;
413   }
414
415   void onCopy() {
416     final int row = getSelectedRow();
417     if (row < 0) {
418       return;
419     }
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);
426   }
427
428   @Override
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();
435       }
436       return true;
437     }
438     return super.onKeyDown(keyCode, event);
439   }
440
441   @Override
442   protected void onListItemClick(ListView l, View v, int row, long id) {
443     setSelectedRow(row);
444     openContextMenu(getListView());
445   }
446
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);
452     }
453   }
454
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);
462   }
463
464   // ----------------------------------------------------------------
465   // ContextMenu
466   // ----------------------------------------------------------------
467
468   @Override
469   public void onCreateContextMenu(ContextMenu menu, View v,
470       ContextMenuInfo menuInfo) {
471     final int row = getSelectedRow();
472     if (row < 0) {
473       return;
474     }
475
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();
480         return false;
481       }
482     });
483
484     final MenuItem copy = menu.add(android.R.string.copy);
485     copy.setOnMenuItemClickListener(new OnMenuItemClickListener() {
486       public boolean onMenuItemClick(MenuItem item) {
487         onCopy();
488         return false;
489       }
490     });
491
492   }
493
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);
499       return;
500     }
501     setSelection(rowIndex);
502     setSelectedRow(rowIndex);
503     getListView().setSelected(true);
504   }
505   
506   // TODO: delete me somehow.
507   private int getSelectedRow() {
508     return lastSelectedRow;
509   }
510   private void setSelectedRow(final int row) {
511     lastSelectedRow = row;
512     Log.d(LOG, "Selected: " + getSelectedRowRawText(true));
513     updateSearchText();
514   }
515
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);
526         }
527       } else {
528         Log.w(LOG, "updateSearchText: nothing selected.");
529       }
530     }
531   }
532
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);
546   }
547
548   class LanguageListAdapter extends BaseAdapter {
549
550     // Visible for testing.
551     final LanguageData languageData;
552
553     LanguageListAdapter(final LanguageData languageData) {
554       this.languageData = languageData;
555     }
556
557     public int getCount() {
558       return languageData.rows.size();
559     }
560
561     public Dictionary.Row getItem(int rowIndex) {
562       assert rowIndex < languageData.rows.size();
563       return languageData.rows.get(rowIndex);
564     }
565
566     public long getItemId(int rowIndex) {
567       return rowIndex;
568     }
569
570     public View getView(final int rowIndex, final View convertView,
571         final ViewGroup parent) {
572       final Row row = getItem(rowIndex);
573
574       // Token row.
575       if (row.isToken()) {
576         TextView result = null;
577         if (convertView instanceof TextView) {
578           result = (TextView) convertView;
579         } else {
580           result = new TextView(parent.getContext());
581         }
582         if (row == null) {
583           return result;
584         }
585         result.setText(languageData.rowToString(row, false));
586         result.setTextAppearance(parent.getContext(),
587             android.R.style.TextAppearance_Large);
588         result.setClickable(false);
589         return result;
590       }
591
592       // Entry row(s).
593       final TableLayout result = new TableLayout(parent.getContext());
594
595       final Entry 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());
599
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;
604
605         if (r > 0) {
606           final TextView spacer = new TextView(tableRow.getContext());
607           spacer.setText(r == 0 ? "\95 " : " \95 ");
608           tableRow.addView(spacer);
609         }
610         tableRow.addView(column1, layoutParams);
611         if (r > 0) {
612           final TextView spacer = new TextView(tableRow.getContext());
613           spacer.setText(r == 0 ? "\95 " : " \95 ");
614           tableRow.addView(spacer);
615         }
616         tableRow.addView(column2, layoutParams);
617
618         column1.setWidth(1);
619         column2.setWidth(1);
620         // column1.setTextAppearance(parent.getContext(), android.R.style.Text);
621
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();
626         int startPos = 0;
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();
632         }
633
634         column2.setText(
635             entry.getAllText(Entry.otherLang(languageData.lang))[r],
636             TextView.BufferType.NORMAL);
637
638         result.addView(tableRow);
639       }
640
641       return result;
642     }
643
644   } // DictionaryListAdapter
645
646   private final class SearchOperation implements Runnable {
647     SearchOperation previousSearchOperation;
648     
649     final LanguageListAdapter listAdapter;
650     final LanguageData languageData;
651     final String searchText;
652     final AtomicBoolean interrupted = new AtomicBoolean(false);
653     boolean searchFinished = false;
654
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;
661     }
662
663     public void run() {
664       if (previousSearchOperation != null) {
665         previousSearchOperation.stopAndWait();
666       }
667       previousSearchOperation = null;
668       
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);
673         
674         Log.d(LOG, "SearchOperation completed: " + indexEntry.toString());
675         uiHandler.post(new Runnable() {
676           public void run() {
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);
681             }
682             synchronized (DictionaryActivity.this) {
683               searchOperation = null;
684               DictionaryActivity.this.notifyAll();
685             }
686           }
687         });
688       }      
689       synchronized (this) {
690         searchFinished = true;
691         this.notifyAll();
692       }
693     }
694     
695     private void stopAndWait() {
696       interrupted.set(true);
697       synchronized (this) {
698         while (!searchFinished) {
699           Log.d(LOG, "stopAndWait: " + searchText);
700           try {
701             this.wait();
702           } catch (InterruptedException e) {
703             Log.e(LOG, "Interrupted", e);
704           }
705         }
706       }
707     }
708   }  // SearchOperation
709
710   void waitForSearchEnd() {
711     synchronized (this) {
712       while (searchOperation != null) {
713         Log.d(LOG, "waitForSearchEnd");
714         try {
715           this.wait();
716         } catch (InterruptedException e) {
717           Log.e(LOG, "Interrupted.", e);
718         }
719       }
720     }
721   }
722
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());
729       }
730     }
731
732     public void beforeTextChanged(CharSequence arg0, int arg1, int arg2,
733         int arg3) {
734     }
735
736     public void onTextChanged(CharSequence arg0, int arg1, int arg2, int arg3) {
737     }
738   }
739
740 }